/*最近公共祖先(LCA)

* 1.向上标记法O(n)
    (1) 从x向上走到根节点，并标记所有经过的结点
    (2)从y向上走到根节点，当第一次遇到已经标记的结点时，就找到了LCA(x,y);

* 2.树上倍增法O((n+m)logn):预处理O(nlogn),每次查询(logn)
    核心数组fa[i][j]:表示从i结点开始，向上走2^j远的祖先结点, 0<=j<=logn
    (1)bfs得到每个结点所处的深度depth[]。根节点为1，越界结点fa[0]=0, depth[x]表示x的深度。
    (2)用二进制拆分，找到a的，b同深度的祖先。（假设a深度大于b）。
    (3)如果此时a==b，那么b是a祖先结点，此时a就是LCA(a,b);
    (4)如果此时a!=b,我们仍然二进制拆分，使得fa[a][i] == fa[b][i];
    (5)此时 a、b,是最近公共祖先的子节点。fa[a][0] == fa[b][0] == LCA(a,b);

* 3.(tarjan求最近公共祖先) O(n+m)
    tarjan是一种离线查询算法
    所谓离线就是将所有查询先记录下来，然后计算好所有的中间过程，最后再将所有的查询记录对应的结果依次输出

    tarjan是基于向上标记法的一种优化
    首先树上两个结点之间的距离是等于 结点u到根节点的距离 + 结点v到根节点的距离 - 2 * 结点u、v的最近公共祖先到根节点的距离 
                                  <-> dist[u,v] = dist[u] + dist[v] - 2 * dist[LCA(u,v)]

    tarjan算法如何求解最近公共祖先:
    首先它将整棵树上的结点分为三部分
    (1)已经搜索完且回溯过的点 标记为 2
    (2)正在搜索的分支结点 标记为 1
    (3)还未搜索到的结点 标记为 0

    * 核心：
        对于已经被标记为 2 的点 和当前正在搜索的分支存在一种关系，就是
        (1)所有被标记为 2 的点都可以合并压缩到当前正在搜索的分支中的某个结点上，这一步操作可以用并查集实现
        (2)因此当我们搜索完且回溯当前结点的时候，可以遍历关于当前结点u的所有有关询问，
           其中询问中对标记为 2 的结点与当前结点u的最近公共祖先就是标记为 2 的结点在并查集中的代表元素（根节点/祖宗）

* 4.严格次小生成树
    枚举每一组非树边，重复3步骤，最后维护一个全局最小值即是次小生成树
    * 步骤: 
    (1)首先用kruskal求出最小生成树的节点和边,并且将这些点和边建成邻接表
    (2)用倍增法预处理出fa,d1和d2数组的值，其中计算d1和d2的值和计算fa数组的值差不多都是使用递推的方式
    (3)预处理完之后就开始用非树边的权值替换掉树边的最大或次大权值，在将节点a和b节点倍增到同一层的过程中，
        顺便记录下途中经过路径的d1和d2的值，跳到同一层之后开始同步跳直到跳到祖宗节点为止，过程中也记录下d1和d2的值，最后选出最小的d1和d2的值，并且用非树边替换掉它。
    (4)枚举每一组非树边，重复3步骤，最后维护一个全局最小值即是次小生成树

    * 核心思路:
    就是使用倍增法计算出从a到祖宗节点和b到祖宗节点这两段路径中树边的最大权值dist1和次大权值dist2，
    最后枚举每一组非树边，例如对于其中一组a−>b,替换掉dist1或者dist2（加入一条非树边减去一条树边），最后维护全局的最小值即是次小生成树
    
* 注意点：
    (1)lca中的第一个for，目的是找到a的，和b同深度的祖先结点。一定要从大到小遍历。
    (2)lca中的第二个for，目的是让a和b分别转移到LCA(a,b)的子节点上，一定要从大到小遍历。
    (3)本题中lca第二个for从小到大遍历也能过，是因为没要求输出正确LCA(a,b),只要求判断。
    (4)第一for目的是让距离t变为0，第二个for目的是让t变为1。
    (5)我们只有从最高位1遍历，才能根据唯一的二进制形式达到要求。

*/

#pragma GCC optimize("O1,O2,O3,Ofast")
#pragma GCC optimize("no-stack-protector,unroll-loops,fast-math,inline")
#pragma GCC target("avx,avx2,fma")
#pragma GCC target("sse,sse2,sse3,sse4,sse4.1,sse4.2,ssse3")

#include<iostream>
#include<cstring>
#include<algorithm>
#include<queue>
// #define ONLINE_GUDGE
using namespace std;
using LL = long long;

const int N = 100010, M = 300010, INF = 0x3f3f3f3f;

//存储边的信息
struct Edge
{
    int a, b, w;
    //判断是不是树边
    bool used;
    bool operator < (const Edge &W) const
    {
        return w < W.w;
    }
}edge[M];

int n, m;
int h[N], w[M], e[M], ne[M], idx;
int p[N];
int depth[N], fa[N][17], d1[N][17], d2[N][17]; // d1最大边 d2次大边

void add(int a, int b, int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++;
}

int find(int x)
{
    if (p[x] != x ) p[x] = find(p[x]);
    return p[x];
}


LL kruskal()
{
    LL sum = 0; // 存储最小生成树的总权重

    //初始化并查集
    for (int i = 1; i <= n; i ++ ) p[i] = i;

    sort(edge, edge + m);

    for (int i = 0; i < m; i ++ )
    {
        int a = edge[i].a, b = edge[i].b, w = edge[i].w;
        int pa = find(a), pb = find(b);

        //如果是树边，则更新边的状态与总权重
        if (pa != pb )
        {
            p[pa] = pb;
            sum += w;
            edge[i].used = true;
        }
    }

    return sum;
}

void build()
{
    memset(h, -1, sizeof h);
    idx = 0;

    for (int i = 0; i < m; i ++ )
        if (edge[i].used )
        {
            int a = edge[i].a, b = edge[i].b, w = edge[i].w;
            add(a, b, w), add(b, a, w);
        }
}

//下面两个函数分别为lca的初始化和执行：

void bfs()
{
    memset(depth, 0x3f, sizeof depth);
    depth[1] = 1, depth[0] = 0;
    queue<int> q;
    q.push(1);

    while (q.size())
    {
        auto u = q.front(); q.pop();

        for (int i = h[u]; ~i; i = ne[i] )
        {
            int v = e[i];

            //结点v深度更深则更新两个结点的状态
            if (depth[v] > depth[u] + 1 )
            {
                depth[v] = depth[u] + 1;

                // 更新v往上跳一步的结点, 即直接的父节点
                fa[v][0] = u;
                q.push(v);
                
                d1[v][0] = w[i], d2[v][0] = -INF; // 更新到往上跳一步的最大和次大边长
                //不断往上跳的一个倍增优化的dp过程
                //为了找到从当前结点到根节点过程中每一部分的最大和次大边长
                for (int k = 1; k <= 16; k ++ )
                {
                    int anc = fa[v][k - 1];
                    fa[v][k] = fa[anc][k - 1];

                    //存储从当前结点跳k — 1步和从跳了k - 1步的结点再往上跳k - 1步(即2^k步)两个过程中的最大和次大边长
                    //通过这个方法可以更快速地更新跳k步这整个过程中的最大和次大边长
                    //为什么要存储次长边而不只是存储最长边：是因为不只是可以更新的最长边+某个正数 实现严格次小
                    //还存在可以通过更新次长边来实现严格次小（更新后的次长边 == 最长边）
                    //Q2: 为什么不存储更小的边：如果某条非树边能替换更小的树边，那么这条边一定是比最长边严格小的，不符合最小生成树的定义
                    int distance[4] = {d1[v][k - 1], d2[v][k - 1], d1[anc][k - 1], d2[anc][k - 1]};
                    d1[v][k] = d2[v][k] = -INF;

                    for (int u = 0; u < 4; u ++ )
                    {
                        int d = distance[u];
                        //注意要满足严格的最长和次长关系
                        if (d > d1[v][k] ) d2[v][k] = d1[v][k], d1[v][k] = d;
                        else if (d != d1[v][k] && d > d2[v][k] ) d2[v][k] = d;
                    }
                }
            }
        }
    }
}


LL lca(int a, int b, int w)
{
    static int distance[N * 2];
    int cnt = 0;

    if (depth[a] < depth[b] ) swap(a, b);

    for (int k = 16; k >= 0; k -- )       
        if (depth[fa[a][k]] >= depth[b] ) // 这里一定注意比较的是a的直接父节点和b的深度，所以是>=号
        {
            distance[cnt ++] = d1[a][k];
            distance[cnt ++] = d2[a][k];
            a = fa[a][k];
        }

    if (a != b )
    {
        for (int k = 16; k >= 0; k -- )
            if (fa[a][k] != fa[b][k] )
            {
                distance[cnt ++] = d1[a][k];
                distance[cnt ++] = d2[a][k];
                distance[cnt ++] = d1[b][k];
                distance[cnt ++] = d2[b][k];
                a = fa[a][k], b = fa[b][k];
            }

        //这里只往上跳一步且为同一个父节点，记住a和b分别到父节点最长边即可
        distance[cnt ++] = d1[a][0];
        distance[cnt ++] = d1[b][0];
    }

    int dist1 = -INF, dist2 = -INF;

    //往上跳过程中所有部分的最长和次长边中找到整棵树的最长和次长边
    for (int i = 0; i < cnt; i ++ )
    {
        int d = distance[i];
        if (d > dist1 ) dist2 = dist1, dist1 = d;
        else if (d != dist1 && d > dist2 ) dist2 = d; 
    }

    //当前备选的边先尝试和最长边比较能否替换，再尝试能否替换次长边
    //因为是严格次小生成树，所以一定替换后要让权重增大所以必须是>不能带=
    if (w > dist1 ) return w - dist1;
    if (w > dist2 ) return w - dist2;
    return INF;
}

int main()
{

    #ifdef ONLINE_JUDGE

    #else
    freopen("./in.txt","r",stdin);
    #endif
    ios::sync_with_stdio(false);   
	cin.tie(0);

    cin >> n >> m;

    for (int i = 0; i < m; i ++ )
    {
        int a, b, w;
        cin >> a >> b >> w;
        edge[i] = {a, b, w};
    }

    LL sum = kruskal();
    build();
    bfs();

    LL res = 1e18;

    //枚举每一条非树边尝试替换最小生成树中的最长或者次长边
    //选出最小的为严格次小生成树
    for (int i = 0; i < m; i ++ )
        if (!edge[i].used )
        {
            int a = edge[i].a, b = edge[i].b, w = edge[i].w;
            res = min(res, sum + lca(a, b, w));
        }

    cout << res << endl;

    return 0;
}